﻿// Copyright 2010 Maxime Serrano <lastchronicler@gmail.com>
// Copyright 2009, 2010 Matvei Stefarov <me@matvei.org>
using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Net;
using System.Collections.Generic;
using zlib;

namespace MCSharp {
    // Protocol encoder for outgoing packets
    sealed class PacketWriter : BinaryWriter {

        public static short SwapEndian(short num) {
            return (short)((num & 0x00FF) << 8 | (num & 0xFF00) >> 8);
        }

        public static int SwapEndian(UInt32 num) {
            return (int)((num & 0x000000FF) << 24 | (num & 0x0000FF00) << 8 | (num & 0x00FF0000) >> 8 | (num & 0xFF000000) >> 24);
        }

        public PacketWriter(Stream stream) : base(stream) { }

        #region Direct Writing
        public void Write(SMSG opcode) {
            Write((byte)opcode);
        }

        public override void Write(short data) {
            base.Write(IPAddress.HostToNetworkOrder(data));
        }

        public override void Write(int data) {
            base.Write(IPAddress.HostToNetworkOrder(data));
        }

        public override void Write(string data) {
            Write(Encoding.ASCII.GetBytes(data.PadRight(64).Substring(0, 64)));
        }

        public void Write(Packet packet) {
            Write(packet.data);
        }

        #endregion
        #region Packet Making

        internal static Packet MakeKeepAlive() {
            Packet packet = new Packet(1);
            packet.data[0] = (byte)SMSG.KeepAlive;
            return packet;
        }

        internal static Packet MakeHandshake(string serverID) {
            short strLen = (short)serverID.Length;
            Packet packet = new Packet(1 + 2 + strLen);
            packet.data[0] = (byte)SMSG.Handshake;
            ToNetOrder(strLen, packet.data, 1);
            Encoding.UTF8.GetBytes(serverID, 0, strLen, packet.data, 3);
            return packet;
        }

        internal static Packet MakeLogin(Player player, string serverName, string MOTD) {
            short nameLen = (short)serverName.Length;
            short motdLen = (short)MOTD.Length;
            Packet packet = new Packet(1 + 4 + 2 + nameLen + 2 + motdLen);
            packet.data[0] = (byte)SMSG.Login;
            ToNetOrder(player.id, packet.data, 1);
            ToNetOrder(nameLen, packet.data, 5);
            Encoding.UTF8.GetBytes(serverName, 0, nameLen, packet.data, 7);
            ToNetOrder(motdLen, packet.data, 7 + nameLen);
            Encoding.UTF8.GetBytes(MOTD, 0, motdLen, packet.data, 7 + nameLen + 2);
            return packet;
        }

        internal static Packet MakePreChunk(int x, int z, bool load) {
            Packet packet = new Packet(1 + 4 + 4 + 1);
            packet.data[0] = (byte)SMSG.PreChunk;
            ToNetOrder((int)x, packet.data, 1);
            ToNetOrder((int)z, packet.data, 5);
            packet.data[9] = (byte)(load ? 1 : 0);
            return packet;
        }

        internal static Packet MakeChunk(Chunk chunk) {
            byte[] ChunkArray = null;
            lock (chunk.blockslock) {
                using (MemoryStream memStream = new MemoryStream()) {
                    using (ZOutputStream compressor = new ZOutputStream(memStream, zlibConst.Z_BEST_COMPRESSION)) {
                        // Write Blocks
                        for (int i = 0; i < (16 * 16 * 128); i++) {
                            compressor.WriteByte(chunk.blocks[i].type);
                        }

                        // Write MetaData
                        for (int i = 0; i < (16 * 16 * 128 / 2); i++) {
                            compressor.WriteByte(((chunk.blocks[(i * 2) + 1].metadata & 0x0F) << 4) | (chunk.blocks[(i * 2) + 0].metadata & 0x0F));
                        }

                        // Write BlockLight
                        for (int i = 0; i < (16 * 16 * 128 / 2); i++) {
                            compressor.WriteByte(0);
                            //compressor.WriteByte(((chunk.blocks[(i * 2) + 1].lighting & 0x0F) << 4) | (chunk.blocks[(i * 2) + 0].lighting & 0x0F));
                        }

                        // Write SkyLight
                        for (int i = 0; i < (16 * 16 * 128 / 2); i++) {
                            compressor.WriteByte(((chunk.blocks[(i * 2) + 1].lighting & 0x0F) << 4) | (chunk.blocks[(i * 2) + 0].lighting & 0x0F));
                        }
                    }
                    ChunkArray = memStream.ToArray();
                }
            }

            Packet packet = new Packet(1 + 4 + 2 + 4 + 1 + 1 + 1 + 4 + ChunkArray.Length);
            packet.data[0] = (byte)SMSG.Chunk;
            ToNetOrder((int)(chunk.x * 16), packet.data, 1);    // X pos (in blocks)
            ToNetOrder((short)0, packet.data, 5);               // Y pos (in blocks)
            ToNetOrder((int)(chunk.z * 16), packet.data, 7);    // Z pos (in blocks)
            packet.data[11] = 15;   // X size - 1
            packet.data[12] = 127;  // Y size - 1 (height)
            packet.data[13] = 15;   // Z size - 1
            ToNetOrder((int)ChunkArray.Length, packet.data, 14);            // ChunkArray length
            Array.Copy(ChunkArray, 0, packet.data, 18, ChunkArray.Length);  // ChunkData 
            return packet;
        }

        internal static Packet MakeInventoryAddition(short type, byte amount, short life) {
            Packet packet = new Packet(6);
            packet.data[0] = (byte)SMSG.AddInventory;
            ToNetOrder(type, packet.data, 1);
            packet.data[3] = amount;
            ToNetOrder(life, packet.data, 4);
            return packet;
        }

        internal static Packet MakeSwitch(Entity player, short Type) {
            Packet packet = new Packet(7);
            packet.data[0] = (byte)SMSG.Switch;
            ToNetOrder((int)player.id, packet.data, 1);
            ToNetOrder((short)Type, packet.data, 5);
            return packet;
        }

        internal static Packet MakeMysteryBool(bool value) {
            Packet packet = new Packet(2);
            packet.data[0] = (byte)SMSG.Bool;
            packet.data[1] = (value ? (byte)0x01 : (byte)0x00);
            return packet;
        }

        internal static Packet MakeTimeUpdate(long time) {
            Packet packet = new Packet(9);
            packet.data[0] = (byte)SMSG.Time;
            byte[] timeArr = BitConverter.GetBytes(time);
            Array.Reverse(timeArr);
            Array.Copy(timeArr, 0, packet.data, 1, 8);
            return packet;
        }

        internal static Packet MakePositionAndLook(Position pos) {
            Packet packet = new Packet(42);
            packet.data[0] = (byte)CMSG.MoveLook;
            byte[] TempArray;

            TempArray = BitConverter.GetBytes((double)pos.x);
            Array.Reverse(TempArray);
            Array.Copy(TempArray, 0, packet.data, 1, 8);

            TempArray = BitConverter.GetBytes((double)pos.y);
            Array.Reverse(TempArray);
            Array.Copy(TempArray, 0, packet.data, 9, 8);

            TempArray = BitConverter.GetBytes((double)pos.y);
            Array.Reverse(TempArray);
            Array.Copy(TempArray, 0, packet.data, 17, 8);

            TempArray = BitConverter.GetBytes((double)pos.z);
            Array.Reverse(TempArray);
            Array.Copy(TempArray, 0, packet.data, 25, 8);

            TempArray = BitConverter.GetBytes((float)pos.rotation);
            Array.Reverse(TempArray);
            Array.Copy(TempArray, 0, packet.data, 33, 4);

            TempArray = BitConverter.GetBytes((float)pos.pitch);
            Array.Reverse(TempArray);
            Array.Copy(TempArray, 0, packet.data, 37, 4);

            packet.data[41] = 1;

            return packet;
        }

        internal static Packet MakeRelativeMove(Entity entity, Position delta) {
            Packet packet = new Packet(8);
            packet.data[0] = (byte)SMSG.RelativeMove;
            ToNetOrder(entity.id, packet.data, 1);
            packet.data[5] = (byte)(delta.x * 32);
            packet.data[6] = (byte)(delta.y * 32);
            packet.data[7] = (byte)(delta.z * 32);
            return packet;
        }

        internal static Packet MakeLook(Entity entity) {
            Packet packet = new Packet(7);
            packet.data[0] = (byte)SMSG.EntityLook;
            ToNetOrder(entity.id, packet.data, 1);
            packet.data[5] = (byte)(entity.pos.rotation * 255 / 360);
            packet.data[6] = (byte)(entity.pos.pitch * 255 / 360);
            return packet;
        }

        internal static Packet MakeRelativeMoveLook(Entity entity, Position delta) {
            Packet packet = new Packet(10);
            packet.data[0] = (byte)SMSG.RelativeMoveLook;
            ToNetOrder(entity.id, packet.data, 1);
            packet.data[5] = (byte)(delta.x * 32);
            packet.data[6] = (byte)(delta.y * 32);
            packet.data[7] = (byte)(delta.z * 32);
            packet.data[8] = (byte)(entity.pos.rotation * 255 / 360);
            packet.data[9] = (byte)(entity.pos.pitch * 255 / 360);
            return packet;
        }

        internal static Packet MakeTeleport(Entity entity) {
            Packet packet = new Packet(19);
            packet.data[0] = (byte)SMSG.Teleport;
            ToNetOrder(entity.id, packet.data, 1);
            ToNetOrder((int)(entity.pos.x * 32), packet.data, 5);
            ToNetOrder((int)(entity.pos.y * 32), packet.data, 9);
            ToNetOrder((int)(entity.pos.z * 32), packet.data, 13);
            packet.data[17] = (byte)(entity.pos.rotation * 255 / 360);
            packet.data[18] = (byte)(entity.pos.pitch * 255 / 360);
            return packet;
        }

        internal static Packet MakeMessage(string message) {
            int codeCount = 0;
            for (int i = 0; i < message.Length; i++)
                if (message[i] == '§')
                    codeCount++;
            short strLen = (short)(message.Length + codeCount);
            Packet packet = new Packet(3 + strLen + codeCount);
            packet.data[0] = (byte)SMSG.Chat;
            ToNetOrder((strLen), packet.data, 1);
            Encoding.UTF8.GetBytes(message, 0, strLen - codeCount, packet.data, 3);
            return packet;
        }

        internal static Packet MakeEntitySpawn(Player player) {
            short nameLen = (short)player.nick.Length;
            Packet packet = new Packet(1 + 4 + 2 + nameLen + 4 + 4 + 4 + 1 + 1 + 2);
            packet.data[0] = (byte)SMSG.NamedEntitySpawn;
            ToNetOrder(player.id, packet.data, 1);
            ToNetOrder(nameLen, packet.data, 5);
            Encoding.UTF8.GetBytes(player.nick, 0, nameLen, packet.data, 7);
            ToNetOrder((int)player.pos.x * 32, packet.data, 7 + nameLen + 0);
            ToNetOrder((int)player.pos.y * 32, packet.data, 7 + nameLen + 4);
            ToNetOrder((int)player.pos.z * 32, packet.data, 7 + nameLen + 8);
            packet.data[7 + nameLen + 12] = (byte)(player.pos.rotation * 255 / 360);
            packet.data[7 + nameLen + 13] = (byte)(player.pos.pitch * 255 / 360);
            ToNetOrder((short)player.currentItem, packet.data, 7 + nameLen + 14);
            return packet;
        }

        internal static Packet MakeDisconnect(string reason) {
            short strLen = (short)reason.Length;
            Packet packet = new Packet(3 + strLen);
            packet.data[0] = (byte)SMSG.Disconnect;
            ToNetOrder(strLen, packet.data, 1);
            Encoding.UTF8.GetBytes(reason, 0, strLen, packet.data, 3);
            return packet;
        }
        
        internal static Packet MakeDestroyEntity(Entity entity) {
            Packet packet = new Packet(5);
            packet.data[0] = (byte)SMSG.DestroyEntity;
            ToNetOrder(entity.id, packet.data, 1);
            return packet;
        }

        internal static Packet MakeArmAnimation(Entity entity) {
            Packet packet = new Packet(6);
            packet.data[0] = (byte)SMSG.ArmAnimation;
            ToNetOrder(entity.id, packet.data, 1);
            packet.data[5] = 1;
            return packet;
        }

        internal static Packet MakeBlockDig(byte status, int x, byte y, int z) {
            Packet packet = new Packet(12);
            packet.data[0] = (byte)SMSG.BlockDig;
            packet.data[1] = status;
            ToNetOrder(x, packet.data, 2);
            packet.data[6] = y;
            ToNetOrder(z, packet.data, 7);
            packet.data[11] = 1;
            return packet;
        }

        internal static Packet MakeBlockChange(ChunkBlock block, int x, byte y, int z) {
            Packet packet = new Packet(12);
            packet.data[0] = (byte)SMSG.BlockChange;
            ToNetOrder(x, packet.data, 1);
            packet.data[5] = y;
            ToNetOrder(z, packet.data, 6);
            packet.data[10] = block.type;
            packet.data[11] = block.metadata;
            return packet;
        }

        #endregion
        #region Utilities

        internal static void ToNetOrder(short number, byte[] arr, int offset) {
            arr[offset] = (byte)((number & 0xff00) >> 8);
            arr[offset + 1] = (byte)(number & 0x00ff);
        }

        internal static void ToNetOrder(int number, byte[] arr, int offset) {
            arr[offset + 0] = (byte)((number & 0xff000000) >> 24);
            arr[offset + 1] = (byte)((number & 0x00ff0000) >> 16);
            arr[offset + 2] = (byte)((number & 0x0000ff00) >> 8);
            arr[offset + 3] = (byte)(number & 0x000000ff);
        }

        #endregion

        internal static string[] splitter = new string[] { "&N" };
        internal static IEnumerable<Packet> MakeWrappedMessage(string prefix, string text, bool appendPrefixToFirstLine) {
            if (appendPrefixToFirstLine) text = prefix + text;

            /* STEP 1: Split by lines */
            if (text.Contains("&N")) {
                bool first = true;
                foreach (string subline in text.Split(splitter, StringSplitOptions.None)) {
                    foreach (Packet p in MakeWrappedMessage(prefix, subline, !first)) {
                        yield return p;
                    }
                    first = false;
                }
                yield break;
            }

            /* STEP 2: Replace special colorcodes */
            text = text.Replace("&S", Color.Sys).Replace("&Y", Color.Say).Replace("&R", Color.Announcement).Replace("&H", Color.Help);


            /* STEP 3: Remove consecutive colorcodes */
            for (int i = 0; i < text.Length - 3; i++) {
                if (text[i] == '§' && IsColorCode(text[i + 1]) && text[i + 2] == '§' && IsColorCode(text[i + 3])) {
                    text = text.Substring(0, i) + text.Substring(i + 2);
                    i--;
                }
            }

            /* STEP 4: Split */
            int lastIndex = 0;

            List<string> segments = new List<string>();
            for (int i = 0; i < text.Length; i++) {
                if (IsColorCode(text[i]) && i > 0 && text[i - 1] == '§') {
                    // split at color codes
                    if (i > 1) {
                        segments.Add(text.Substring(lastIndex, i - lastIndex - 1));
                        lastIndex = i - 1;
                    }

                } else if (text[i] == ' ') {
                    for (; i < text.Length && text[i] == ' '; i++) ;
                    i--;
                    // split at spaces
                    segments.Add(text.Substring(lastIndex, i - lastIndex));
                    lastIndex = i;
                }
            }

            // add remainder of the string
            if (lastIndex != text.Length) {
                segments.Add(text.Substring(lastIndex));
            }


            /* STEP 5: Delete empty segments */
            for (int i = segments.Count - 1; i >= 0; i--) {
                if (segments[i].Length == 0) segments.RemoveAt(i);
            }


            /* STEP 6: Join segments into strings */
            string line = "";
            string lastColorCode = "";
            List<string> lines = new List<string>();

            for (int i = 0; i < segments.Count; i++) {
                if (line.Length + segments[i].TrimEnd().Length + 1 > 64) {
                    // end of line, start new one
                    lines.Add(line);

                    if (segments[i].TrimStart().StartsWith("§")) {
                        lastColorCode = segments[i].Substring(0, 2);
                        line = prefix + segments[i].TrimStart();

                    } else {
                        line = prefix + lastColorCode + segments[i].TrimStart();
                    }
                } else {
                    // apending to line
                    if (segments[i].TrimStart().StartsWith("§")) {
                        lastColorCode = segments[i].Substring(0, 2);
                        line += segments[i];
                    } else {
                        line += segments[i];
                    }
                }
            }

            // last line
            lines.Add(line);


            /* STEP 7: Remove trailing whitespace and colorcodes */
            for (int l = lines.Count - 1; l >= 0; l--) {
                int i = lines[l].Length - 1;
                for (; i >= 0 && (lines[l][i] == ' ' || lines[l][i] == '&' || IsColorCode(lines[l][i]) && i > 0 && lines[l][i - 1] == '&'); i--) ;
                if (i == 0) {
                    lines.RemoveAt(l);
                } else {
                    lines[l] = lines[l].Substring(0, i + 1);
                }
            }

            /* STEP 8: DONE */
            foreach (string processedLine in lines) {
                yield return MakeMessage(processedLine);
            }
        }

        static bool IsColorCode(char c) {
            return (c >= '0' && c <= '9' || c >= 'a' && c <= 'f');
        }
    }
}